using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using Velocity4Net.Runtime.ResourceManage.Loader;
using Velocity4Net.Errors;
using Velocity4Net.Runtime.Logging;
using Velocity4Net.Cfg;

namespace Velocity4Net.Runtime.ResourceManage
{

    /**
     * Class to manage the text resource for the Velocity Runtime.
     *
     * @author  <a href="mailto:wglass@forio.com">Will Glass-Husain</a>
     * @author  <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>
     * @author  <a href="mailto:paulo.gaspar@krankikom.de">Paulo Gaspar</a>
     * @author  <a href="mailto:geirm@optonline.net">Geir Magnusson Jr.</a>
     * @author  <a href="mailto:henning@apache.org">Henning P. Schmiedehausen</a>
     * @version  $Id: ResourceManagerImpl.java 745757 2009-02-19 06:48:10Z nbubna $
     */
    public class ResourceManagerImpl
        : ResourceManager
    {

        /** A template resources. */
        public const int RESOURCE_TEMPLATE = 1;

        /** A static content resource. */
        public const int RESOURCE_CONTENT = 2;

        /** token used to identify the loader internally. */
        private const String RESOURCE_LOADER_IDENTIFIER = "_RESOURCE_LOADER_IDENTIFIER_";

        /** Object implementing ResourceCache to be our resource manager's Resource cache. */
        protected ResourceCache globalCache = null;

        /** The List of templateLoaders that the Runtime will use to locate the InputStream source of a template. */
        protected IList resourceLoaders = new ArrayList();

        /**
         * This is a list of the template input stream source initializers, basically properties for a particular template stream
         * source. The order in this list reflects numbering of the properties i.e.
         *
         * <p>&lt;loader-id&gt;.resource.loader.&lt;property&gt; = &lt;value&gt;</p>
         */
        private IList sourceInitializerList = new ArrayList();

        /**
         * Has this Manager been initialized?
         */
        private bool isInit = false;

        /** switch to turn off log notice when a resource is found for the first time. */
        private bool logWhenFound = true;

        /** The internal RuntimeServices object. */
        protected RuntimeServices rsvc = null;

        /** Logging. */
        protected Log log = null;

        /**
         * Initialize the ResourceManager.
         *
         * @param  rsvc  The Runtime Services object which is associated with this Resource Manager.
         */
        public void Initialize(RuntimeServices rsvc)
        {
            if (isInit)
            {
                log.debug("Re-initialization of ResourceLoader attempted and ignored.");
                return;
            }

            ResourceLoader resourceLoader = null;

            this.rsvc = rsvc;
            log = rsvc.getLog();

            log.trace("Default ResourceManager initializing. (" + this.GetType() + ")");

            assembleResourceLoaderInitializers();

            foreach (VelocitySettings configuration in sourceInitializerList)
            {
                /**
                 * Resource loader can be loaded either via class name or be passed
                 * in as an instance.
                 */

                String loaderClass = configuration.GetString("class");
                ResourceLoader loaderInstance = (ResourceLoader)configuration["instance"];

                if (loaderInstance != null)
                {
                    resourceLoader = loaderInstance;
                }
                else if (loaderClass != null)
                {
                    resourceLoader = ResourceLoaderFactory.getLoader(rsvc, loaderClass);
                }
                else
                {
                    String msg = "Unable to find '" +
                              configuration.GetString(RESOURCE_LOADER_IDENTIFIER) +
                              ".resource.loader.class' specification in configuration." +
                              " This is a critical value.  Please adjust configuration.";
                    log.error(msg);
                    throw new VelocityException(msg);
                }

                resourceLoader.commonInit(rsvc, configuration);
                resourceLoader.Init(configuration);
                resourceLoaders.Add(resourceLoader);
            }

            /*
             * now see if this is overridden by configuration
             */

            logWhenFound = rsvc.GetBoolean(RuntimeConstants.RESOURCE_MANAGER_LOGWHENFOUND, true);

            /*
             *  now, is a global cache specified?
             */

            String cacheClassName = rsvc.GetString(RuntimeConstants.RESOURCE_MANAGER_CACHE_CLASS);

            Object cacheObject = null;

            if (!string.IsNullOrEmpty(cacheClassName))
            {
                try
                {
                    cacheObject = Activator.CreateInstance(Type.GetType(cacheClassName));
                }
                catch (TypeLoadException cnfe)
                {
                    String msg = "The specified class for ResourceCache (" + cacheClassName +
                              ") does not exist or is not accessible to the current classloader.";
                    log.error(msg, cnfe);
                    throw new VelocityException(msg, cnfe);
                }
                catch (ArgumentException ae)
                {
                    throw new VelocityException("Could not access class '"
                        + cacheClassName + "'", ae);
                }
                catch (NotSupportedException ie)
                {
                    throw new VelocityException("Could not instantiate class '"
                        + cacheClassName + "'", ie);
                }

                if (!(cacheObject is ResourceCache))
                {
                    String msg = "The specified resource cache class (" + cacheClassName +
                              ") must implement " + typeof(ResourceCache).Name;
                    log.error(msg);
                    throw new ApplicationException(msg);
                }
            }

            /*
             *  if we didn't get through that, just use the default.
             */
            if (cacheObject == null)
            {
                cacheObject = new ResourceCacheImpl();
            }

            globalCache = (ResourceCache)cacheObject;

            globalCache.initialize(rsvc);

            log.trace("Default ResourceManager initialization complete.");
        }

        /**
         * This will produce a List of Hashtables, each hashtable contains the intialization info for a particular resource loader. This
         * Hashtable will be passed in when initializing the the template loader.
         */
        private void assembleResourceLoaderInitializers()
        {
            List<object> resourceLoaderNames = rsvc.Configuration.GetVector(RuntimeConstants.RESOURCE_LOADER);
            //StringUtils.trimStrings(resourceLoaderNames);

            foreach (String loaderName in resourceLoaderNames)
            {

                /*
                 * The loader id might look something like the following:
                 *
                 * file.resource.loader
                 *
                 * The loader id is the prefix used for all properties
                 * pertaining to a particular loader.
                 */
                StringBuilder loaderID = new StringBuilder(loaderName);
                loaderID.Append(".").Append(RuntimeConstants.RESOURCE_LOADER);

                VelocitySettings loaderConfiguration =
                    rsvc.Configuration.Subset(loaderID.ToString());

                /*
                 *  we can't really count on ExtendedProperties to give us an empty set
                 */
                if (loaderConfiguration == null)
                {
                    log.debug("ResourceManager : No configuration information found " +
                              "for resource loader named '" + loaderName +
                              "' (id is " + loaderID + "). Skipping it...");
                    continue;
                }

                /*
                 *  add the loader name token to the initializer if we need it
                 *  for reference later. We can't count on the user to fill
                 *  in the 'name' field
                 */

                loaderConfiguration.SetProperty(RESOURCE_LOADER_IDENTIFIER, loaderName);

                /*
                 * Add resources to the list of resource loader
                 * initializers.
                 */
                sourceInitializerList.Add(loaderConfiguration);
            }
        }

        /**
         * Gets the named resource. Returned class type corresponds to specified type (i.e. <code>Template</code> to <code>
         * RESOURCE_TEMPLATE</code>).
         *
         * This method is now unsynchronized which requires that ResourceCache
         * implementations be thread safe (as the default is).
         *
         * @param  resourceName  The name of the resource to retrieve.
         * @param  resourceType  The type of resource (<code>RESOURCE_TEMPLATE</code>, <code>RESOURCE_CONTENT</code>, etc.).
         * @param  encoding  The character encoding to use.
         *
         * @return  Resource with the template parsed and ready.
         *
         * @throws  ResourceNotFoundException  if template not found from any available source.
         * @throws  ParseErrorException  if template cannot be parsed due to syntax (or other) error.
         */
        public Resource GetResource(String resourceName, int resourceType, String encoding)
        {
            /*
             * Check to see if the resource was placed in the cache.
             * If it was placed in the cache then we will use
             * the cached version of the resource. If not we
             * will load it.
             *
             * Note: the type is included in the key to differentiate ContentResource
             * (static content from #include) with a Template.
             */

            String resourceKey = resourceType + resourceName;
            Resource resource = globalCache.get(resourceKey);

            if (resource != null)
            {
                try
                {
                    // avoids additional method call to refreshResource
                    if (resource.RequiresChecking())
                    {
                        /*
                         * both loadResource() and refreshResource() now return
                         * a new Resource instance when they are called
                         * (put in the cache when appropriate) in order to allow
                         * several threads to parse the same template simultaneously.
                         * It is redundant work and will cause more garbage collection but the
                         * benefit is that it allows concurrent parsing and processing
                         * without race conditions when multiple requests try to
                         * refresh/load the same template at the same time.
                         *
                         * Another alternative is to limit template parsing/retrieval
                         * so that only one thread can parse each template at a time
                         * but that creates a scalability bottleneck.
                         *
                         * See VELOCITY-606, VELOCITY-595 and VELOCITY-24
                         */
                        resource = refreshResource(resource, encoding);
                    }
                }
                catch (ResourceNotFoundException rnfe)
                {
                    /*
                     *  something exceptional happened to that resource
                     *  this could be on purpose,
                     *  so clear the cache and try again
                     */

                    globalCache.remove(resourceKey);

                    return GetResource(resourceName, resourceType, encoding);
                }
                catch (ParseErrorException pee)
                {
                    log.error("ResourceManager.getResource() exception", pee);
                    throw pee;
                }
                catch (ApplicationException re)
                {
                    log.error("ResourceManager.getResource() exception", re);
                    throw re;
                }
            }
            else
            {
                try
                {
                    /*
                     *  it's not in the cache, so load it.
                     */
                    resource = loadResource(resourceName, resourceType, encoding);

                    if (resource.ResourceLoader.CachingOn)
                    {
                        globalCache.put(resourceKey, resource);
                    }
                }
                catch (ResourceNotFoundException rnfe)
                {
                    log.error("ResourceManager : unable to find resource '" +
                              resourceName + "' in any resource loader.");
                    throw rnfe;
                }
                catch (ParseErrorException pee)
                {
                    log.error("ResourceManager.getResource() parse exception", pee);
                    throw pee;
                }
                catch (ApplicationException re)
                {
                    log.error("ResourceManager.getResource() load exception", re);
                    throw re;
                }
            }

            return resource;
        }

        /**
         * Create a new Resource of the specified type.
         *
         * @param  resourceName  The name of the resource to retrieve.
         * @param  resourceType  The type of resource (<code>RESOURCE_TEMPLATE</code>, <code>RESOURCE_CONTENT</code>, etc.).
         * @return  new instance of appropriate resource type
         * @since 1.6
         */
        protected Resource createResource(String resourceName, int resourceType)
        {
            return ResourceFactory.getResource(resourceName, resourceType);
        }

        /**
         * Loads a resource from the current set of resource loaders.
         *
         * @param  resourceName  The name of the resource to retrieve.
         * @param  resourceType  The type of resource (<code>RESOURCE_TEMPLATE</code>, <code>RESOURCE_CONTENT</code>, etc.).
         * @param  encoding  The character encoding to use.
         *
         * @return  Resource with the template parsed and ready.
         *
         * @throws  ResourceNotFoundException  if template not found from any available source.
         * @throws  ParseErrorException  if template cannot be parsed due to syntax (or other) error.
         */
        protected Resource loadResource(String resourceName, int resourceType, String encoding)
        {
            Resource resource = createResource(resourceName, resourceType);
            resource.RuntimeServices = (rsvc);
            resource.Name = (resourceName);
            resource.CharSet = (encoding);

            /*
             * Now we have to try to find the appropriate
             * loader for this resource. We have to cycle through
             * the list of available resource loaders and see
             * which one gives us a stream that we can use to
             * make a resource with.
             */

            long howOldItWas = 0;

            foreach (ResourceLoader resourceLoader in resourceLoaders)
            {
                resource.ResourceLoader = (resourceLoader);

                /*
                 *  catch the ResourceNotFound exception
                 *  as that is ok in our new multi-loader environment
                 */

                try
                {

                    if (resource.Process())
                    {
                        /*
                         *  FIXME  (gmj)
                         *  moved in here - technically still
                         *  a problem - but the resource needs to be
                         *  processed before the loader can figure
                         *  it out due to to the new
                         *  multi-path support - will revisit and fix
                         */

                        if (logWhenFound && log.isDebugEnabled())
                        {
                            log.debug("ResourceManager : found " + resourceName +
                                      " with loader " +
                                      resourceLoader.ClassName);
                        }

                        howOldItWas = resourceLoader.getLastModified(resource);

                        break;
                    }
                }
                catch (ResourceNotFoundException rnfe)
                {
                    /*
                     *  that's ok - it's possible to fail in
                     *  multi-loader environment
                     */
                }
            }

            /*
             * Return null if we can't find a resource.
             */
            if (resource.Data == null)
            {
                throw new ResourceNotFoundException("Unable to find resource '" + resourceName + "'");
            }

            /*
             *  some final cleanup
             */

            resource.LastModified = (howOldItWas);
            resource.ModificationCheckInterval = (resource.ResourceLoader.ModificationCheckInterval);

            resource.Touch();

            return resource;
        }

        /**
         * Takes an existing resource, and 'refreshes' it. This generally means that the source of the resource is checked for changes
         * according to some cache/check algorithm and if the resource changed, then the resource data is reloaded and re-parsed.
         *
         * @param  resource  resource to refresh
         * @param  encoding  character encoding of the resource to refresh.
         *
         * @throws  ResourceNotFoundException  if template not found from current source for this Resource
         * @throws  ParseErrorException  if template cannot be parsed due to syntax (or other) error.
         */
        protected Resource refreshResource(Resource resource, String encoding)
        {
            /*
             * The resource knows whether it needs to be checked
             * or not, and the resource's loader can check to
             * see if the source has been modified. If both
             * these conditions are true then we must reload
             * the input stream and parse it to make a new
             * AST for the resource.
             */

            /*
             *  touch() the resource to reset the counters
             */
            resource.Touch();

            /* check whether this can now be found in a higher priority
             * resource loader.  if so, pass the request off to loadResource.
             */
            ResourceLoader loader = resource.ResourceLoader;
            if (resourceLoaders.Count > 0 && resourceLoaders.IndexOf(loader) > 0)
            {
                String name = resource.Name;
                if (loader != GetLoaderForResource(name))
                {
                    return loadResource(name, resource.ResourceType, encoding);
                }
            }

            if (resource.SourceModified)
            {
                /*
                 *  now check encoding info.  It's possible that the newly declared
                 *  encoding is different than the encoding already in the resource
                 *  this strikes me as bad...
                 */

                if (resource.CharSet!=encoding)
                {
                    log.warn("Declared encoding for template '" +
                                 resource.Name +
                                 "' is different on reload. Old = '" +
                                 resource.CharSet + "' New = '" + encoding);

                    resource.CharSet = (encoding);
                }

                /*
                 *  read how old the resource is _before_
                 *  processing (=>reading) it
                 */
                long howOldItWas = loader.getLastModified(resource);

                String resourceKey = resource.GetType() + resource.Name;

                /* 
                 * we create a copy to avoid partially overwriting a
                 * template which may be in use in another thread
                 */

                Resource newResource =
                    ResourceFactory.getResource(resource.Name, resource.ResourceType);

                newResource.RuntimeServices=(rsvc);
                newResource.Name=(resource.Name);
                newResource.CharSet=(resource.CharSet);
                newResource.ResourceLoader=(loader);
                newResource.ModificationCheckInterval=(loader.ModificationCheckInterval);

                newResource.Process();
                newResource.LastModified=(howOldItWas);
                resource = newResource;

                globalCache.put(resourceKey, newResource);
            }
            return resource;
        }

        /**
         * Gets the named resource. Returned class type corresponds to specified type (i.e. <code>Template</code> to <code>
         * RESOURCE_TEMPLATE</code>).
         *
         * @param  resourceName  The name of the resource to retrieve.
         * @param  resourceType  The type of resource (<code>RESOURCE_TEMPLATE</code>, <code>RESOURCE_CONTENT</code>, etc.).
         *
         * @return  Resource with the template parsed and ready.
         *
         * @throws  ResourceNotFoundException  if template not found from any available source.
         * @throws  ParseErrorException  if template cannot be parsed due to syntax (or other) error.
         * @throws  Exception  if a problem in parse
         *
         * @deprecated  Use {@link #getResource(String resourceName, int resourceType, String encoding )}
         */
        public Resource GetResource(String resourceName, int resourceType)
        {
            return GetResource(resourceName, resourceType, RuntimeConstants.ENCODING_DEFAULT);
        }

        /**
         * Determines if a template exists, and returns name of the loader that provides it. This is a slightly less hokey way to
         * support the Velocity.templateExists() utility method, which was broken when per-template encoding was introduced. We can
         * revisit this.
         *
         * @param  resourceName  Name of template or content resource
         *
         * @return  class name of loader than can provide it
         */
        public String GetLoaderNameForResource(String resourceName)
        {
            ResourceLoader loader = GetLoaderForResource(resourceName);
            if (loader == null)
            {
                return null;
            }
            return loader.GetType().ToString();
        }

        /**
         * Returns the first {@link ResourceLoader} in which the specified
         * resource exists.
         */
        private ResourceLoader GetLoaderForResource(String resourceName)
        {
            foreach (ResourceLoader loader in resourceLoaders)
            {
                if (loader.resourceExists(resourceName))
                {
                    return loader;
                }
            }
            return null;
        }

    }
}
